Ekaitz's tech blog:
I make stuff at ElenQ Technology and I talk about it

Milestone — Minimal RISC-V support in GCC 4.6.4

From the series: Bootstrapping GCC in RISC-V

In the series we already introduced GCC, its internals, and the work I’m doing to make it able to bootstrap on RISC-V. In this post we are going to tackle the backporting effort and see how I managed to make GCC-4.6.4 compile a simple program to RISC-V.

How to follow this post

As this is going to be deeply connected to the changes I introduced in the codebase, I suggest you to follow it directly in the repository, the branch where I did the changes is riscv, which starts from releases/gcc-4.6.4. As I will continue adding changes on top of this, I left a tag called minimal-compiler that points to the contents of the repository when this blog post was written.

In any case, I’ll share small pieces of the code in the post, but of course I can’t share everything here so I recommend you to go to the sources. I won’t link the sources directly but mention where you can find the changes so you are not forced to follow all the links in the browser and you can use your favorite editor for that.

Overview of the commits

The riscv branch were I made all the work is split in several commits from releases/gcc-4.6.4, where it started.

First it comes a series of 4 commits that make GCC-4.6.4 compilable with more recent toolchains. These should be separated as independent patches later and apply them by the distribution tool, Guix in this case.

Next a couple of commits describe a precarious guix.scm file that should compile the project properly. At the moment it’s not fully ready for distribution but that’s not really our job in the project, so I don’t want to spend a lot of time on that yet. At the moment it’s just working so you can run guix build -f guix.scm from the project directory and it should build a minimal compiler, as we’ll see later. There’s also a channels.scm file, so you can use the exact packages I used thanks to the very powerfull guix time-machine command and replicate my exact build.

Even if I didn’t want to spend a long time with the Guix package, I’d lie to you if I tell you I didn’t. Compiling legacy software is extremely difficult. In this case, I had to patch the code to be compatible with more modern GCC Toolchains, package an old flex, choose lots of configure time options… Still, there are tons of things missing: there’s no C++ support, the package doesn’t find system’s libraries such as glibc and it’s not integrated with system’s binutils. I don’t know how I’m going to fix that to be honest, but I don’t want to think on that right now.

The next commits are what interests us the most: changes on top of GCC.

The first of them1 is just the RISC-V port commit from upstream GCC applied on top of the project, being a little bit careful about conflicts2. Obviously, this change doesn’t really work, it doesn’t even compile, but it serves us to see which changes were needed on top of it.

In the next commit3 I made a high-level fix on the Machine Description files. If you remember from the post about GCC internals, the machine description files are some kind of Lisp-like files that describe both the translations between GIMPLE and RTL and also between RTL and assembly, among other things. In this commit I just removed some of the RTXs that were not available back in the 4.6.4 days but were in use in the port. I’m talking, more specifically, about define_int_iterator and define_int_attr. Thankfully they were just a couple of loops that were easy to unroll by hand. Not a big deal.

Then, I made a larger commit that tries to fix the rest of the gcc/config/riscv folder4. In this one I had two goals: make the port compatible with the old C-based API and remove parts that weren’t strictly necessary but complex to keep. This means I removed all the builtins support so I didn’t need to port them (nice trick, huh?) and I kept the code related with memory models out of the equation. I may need to fix that in the future, but I was looking for a minimal support and I didn’t need that for my goal.

After that I tried to compile the project and run it, but I realized there was a problem with the argument handling of the compiler. It was unable to find arguments like -march and it was always failing to compile anything.

I realized there was a weird file at gcc/common/config/riscv/riscv-common.c that looked like it was handling input arguments, so I focused on porting that one too. It happens that the old GCC didn’t have that code structure: everything was done in gcc/config/ back then, so I moved the support and made the argument handling follow the old API. That’s the last commit of the series5.

Deep diving

Now I’ll try to explain the changes I made in the code here and there, but first I have to explain the method I followed to make this.

It might be surprising but for the first time I didn’t try to understand everything but work my way through it. This means I have absolutely no clue about what does the code do in most of the places6. I just looked the overall shape of it and try to match that shape with the code found in other architecture, mostly MIPS, which the RISC-V support was based on. If I found anything that I didn’t know how to convert I would read how that thing was implemented on MIPS when the RISC-V support was added and then compare that implementation with the one at 4.6.4. That would give me an idea about how to convert to the old way to make things.

So, yeah, most of the coding was a mental exercise of pattern matching code and conversion. There are very few things that I coded myself, like understanding what I was doing deeply.

This doesn’t really mean you don’t need any knowledge to do this. Of course you do. You need to understand what the code does in a very high-level, and know how targets are described in GCC7, but you don’t really need to know each function to the detail.

Sadly, in some cases I had to read functions carefully and understand them, so there’s some knowledge needed, still.

First patch set

The first patch set is not really relevant. I just made it while I was trying to compile the project without changes. The compilation ended with errors, I reviewed them, go to the GCC issue tracker and search. In some cases I was lucky that I found a patch that fixed them, in others I only found suggestions and I had to fix the thing myself. Not really interesting, honestly.

The Guix package

The Guix part in guix.scm is not really interesting neither, at least for the moment. The most interesting part might be the addition of flex-2.5 to the input and the use of local-file as a source for the GCC package8.

All the rest is playing around with the configure flags and trying to read Guix’s GCC packages and Janneke’s work with the full-source bootstrap.

Even with all that, there are some things missing, so I have to come back to this in the future.

There is, though, a really interesting point to take in account. We already said in the post about GCC internals that GCC is a driver that calls other programs, such as as and ld from GNU Binutils, so we know we only need the very basics in order to test that our compiler can output RISC-V assembly so we can ignore the rest of things and focus on one thing: I’m talking, of course, about cc1, the C compiler.

That’s why I only set the target to all-gcc and focus on that. Later we’ll need to dig deeper.

One of the issues I’ll have to tackle is that the GCC I’m building is a cross-compiler, but this whole project is being developed for a RISC-V target. This doesn’t let the compiler check itself using the staged approach9, which is something I’m interested on watching.

Once the proper guix.scm file is generated, I’ll prepare a package for the RISC-V bootstrapping process. In that package I’ll define the first 4 commits as separate patches to apply on top of the source, but I’ll remove them from the original source. That way the codebase will continue to be compatible with old toolchains and we’ll only apply those patches where needed, that is, when we try to build with more recent environments.

Machine Description files

The machine description files did not change that much during the years. Some extra constructs were added but the idea, the goal and the shape of the files didn’t really change.

As we introduced already, the RISC-V port used define_int_iterator constructs in order to simplify some of the work, repeating pieces of the machine description file according to the integer iterator. Back in GCC 4.6.4 that construct was not available so I unrolled the loop by hand following the example at the GCC documentation:

https://gcc.gnu.org/onlinedocs/gccint/Int-Iterators.html

Simply repeat the structures (unroll them) using the value of the iterators and use the define_int_attr to set some of the fields too. The example in the docs gives a good description on how to do it.

On the other hand, I also found that the RTLs at RISC-V port were using simple-return in some places and I realized that didn’t exist in the past. I replaced that with return, hoping that it was the same, but I don’t remember if I reasoned further10. In any case, you can take a look into gcc/rtl.def11 and see how SIMPLE_RETURN was added later.

Matching the API

There are other more meaningful changes. The large commit4 is full of changes related with the conversion back to the C API.

The most obvious ones are converting from rtx_insn * to rtx, and adding/removing machine modes where needed. It was just a matter of searching the functions being used in the MIPS target and trying to match them. Boring, and probably wrong in a couple of places, but looks like it’s working, I don’t know. Examples:

-  emit_insn (gen_rtx_SET (target, src));
+  emit_insn (gen_rtx_SET (VOIDmode, target, src));
-    op = plus_constant (Pmode, UNSPEC_ADDRESS (base), INTVAL (offset));
+    op = plus_constant (UNSPEC_ADDRESS (base), INTVAL (offset));

There were a couple of functions using a small class called cumulative_args_t that it was easy to convert to CUMULATIVE_ARGS * just removing calls to get_cumulative_args and pack_cumulative_args. In C everything is rougher and low level. Thankfully in this case, the low level API was still present so we could just use that instead of the new C++ one, and removing the abstraction level was trivial. See riscv_setup_incoming_varargs in gcc/config/riscv/riscv.c as an example. There might be some things wrong, but it looks reasonable.

There were also a couple of std::swap calls here and there I needed to get rid of. I made a temporary variable and made the swap by hand in the classic way.

Some other changes were harder to spot. Like these:

            || !TYPE_MIN_VALUE (index)
-           || !tree_fits_uhwi_p (TYPE_MIN_VALUE (index))
-           || !tree_fits_uhwi_p (elt_size))
+           || !host_integerp(TYPE_MIN_VALUE (index),0)
+           || !host_integerp(elt_size,0))
          return -1;

-       n_elts = 1 + tree_to_uhwi (TYPE_MAX_VALUE (index))
-                  - tree_to_uhwi (TYPE_MIN_VALUE (index));
+       n_elts = 1 + TREE_INT_CST_LOW(TYPE_MAX_VALUE (index))
+                  - TREE_INT_CST_LOW (TYPE_MIN_VALUE (index));

All those functions and macros are pretty different, but they happen to be more or less the same. What I did here was: read the newer MIPS implementation, try to find those and then go back in time to the old MIPS implementation and see what they were using instead. It wasn’t obvious at the beginning so I read the definitions of all of those things (ctags for the win!) and I even had to define some like sext_hwi, which I added to gcc/hwint.h like I could.

The include dance

If you check the changes on the top of gcc/config/riscv/riscv.c, you’ll see there are a lot of #includes removed and some new ones are added. This is normal, as the older C API was very different to the newer C++ one, but also because many of these includes were not really used inside of the code. First I reviewed which files did exist but later just copied from MIPS and rearranged until the thing compiled.

Crazy changes and inventions

Some other changes were crazier. I had to add the riscv_cpu_cpp_builtins which was defined in gcc/config/riscv/riscv-c.c but I had no way to make it work so I copied what was done in other places and made it a huge macro, added it to gcc/config/riscv/riscv.h and prayed. The compiler was happy with that change, and I was too. That let me remove the riscv-c.c file from the compilation process, even if it’s still included in the repository (yeah, I know…).

The riscv.h file has some other magic tricks too. The ASM_SPEC is a lot of fun now. Basically a copy of somewhere else, because defining the craziest macro I’ve seen in my life was too much for me:

#define ASM_SPEC "\
 %(subtarget_asm_debugging_spec) \
-%{" FPIE_OR_FPIC_SPEC ":-fpic} \
+%{fpic|fPIC|fpie|fPIE:-k}\
 %{march=*} \
 %{mabi=*} \
 %(subtarget_asm_spec)"

Wanna see the macro? Well you asked for it (this is just half of it):

#ifdef ENABLE_DEFAULT_PIE
#define NO_PIE_SPEC     "no-pie|static"
#define PIE_SPEC        NO_PIE_SPEC "|r|shared:;"
#define NO_FPIE1_SPEC       "fno-pie"
#define FPIE1_SPEC      NO_FPIE1_SPEC ":;"
#define NO_FPIE2_SPEC       "fno-PIE"
#define FPIE2_SPEC      NO_FPIE2_SPEC ":;"
#define NO_FPIE_SPEC        NO_FPIE1_SPEC "|" NO_FPIE2_SPEC
#define FPIE_SPEC       NO_FPIE_SPEC ":;"
#define NO_FPIC1_SPEC       "fno-pic"
#define FPIC1_SPEC      NO_FPIC1_SPEC ":;"
#define NO_FPIC2_SPEC       "fno-PIC"
#define FPIC2_SPEC      NO_FPIC2_SPEC ":;"
#define NO_FPIC_SPEC        NO_FPIC1_SPEC "|" NO_FPIC2_SPEC
#define FPIC_SPEC       NO_FPIC_SPEC ":;"
#define NO_FPIE1_AND_FPIC1_SPEC NO_FPIE1_SPEC "|" NO_FPIC1_SPEC
#define FPIE1_OR_FPIC1_SPEC NO_FPIE1_AND_FPIC1_SPEC ":;"
#define NO_FPIE2_AND_FPIC2_SPEC NO_FPIE2_SPEC "|" NO_FPIC2_SPEC
#define FPIE2_OR_FPIC2_SPEC NO_FPIE2_AND_FPIC2_SPEC ":;"
#define NO_FPIE_AND_FPIC_SPEC   NO_FPIE_SPEC "|" NO_FPIC_SPEC
#define FPIE_OR_FPIC_SPEC   NO_FPIE_AND_FPIC_SPEC ":;"

Well anyway, more things were basically made up like that, like these lines in gcc/config/riscv/linux.h:

-#define TARGET_OS_CPP_BUILTINS()                               \
-  do {                                                         \
-    GNU_USER_TARGET_OS_CPP_BUILTINS();                         \
-  } while (0)
+#define TARGET_OS_CPP_BUILTINS()  LINUX_TARGET_OS_CPP_BUILTINS()
   %{!shared: \
     %{!static: \
       %{rdynamic:-export-dynamic} \
-      -dynamic-linker " GNU_USER_DYNAMIC_LINKER "} \
+      -dynamic-linker " LINUX_DYNAMIC_LINKER "} \
     %{static:-static}}"

I just copied from other places because there were absolutely no references to those macros, so… I thought the best way to do this was to copy what other targets did.

Of course this whole thing is not really tested right now, because this affects how the linker is called, but that was broken anyway because of my distribution of choice (Guix I love you but…) so what could I do? Just make them up and fix them later sounded like a good plan.

As I already mentioned, I left builtins and memory models out of the equation. Just commented them out and hoped everything worked properly for small programs. I will try larger programs later.

Argument handling

The last commit5 was a little bit hard to do too, the changes related to this one were adding a file that was completely out of place, as we said earlier, so I reviewed other architectures and found how those architectures dealt with this. First, the API was pretty different so the first thing I made was to make the function’s formal arguments fit those on the API and then started making changes.

It was really hard to realize how the MASK_* macros worked just looking to the code, because there were defined nowhere!

The problem was I wasn’t looking in the correct place. More code generation magic! The gcc/config/riscv/riscv.opt file is what handles all those masks and TARGET_* macros, like TARGET_MUL to check if the target has the multiplication plugin. All those were defined there, even if the definition was obscure and hard to match with anything else in the code12.

Once that was understood everything else was easier to do, “just follow MIPS and you’ll be fine” I told myself, and it worked. Moved everything to riscv.c where all the other target description macros and functions are defined and… Boom! Working compiler.

Result

With all these changes is now possible to generate a minimal compiler and compile a file. As we said, we are only interested on the C to assembly conversion at the moment, and that’s what we have and nothing else.

Taking the project as it is right now you can run:

$ guix build -f guix.scm
...
/gnu/store/gsq72r3xnv7b2f1l4z5idpy3j900hizk-gcc-4.6.4-HEAD-debug
/gnu/store/qglp0cx0nq2nblcg9ya4gmc5gfk2amjg-gcc-4.6.4-HEAD-lib
/gnu/store/l612a4h9a6l4hs7kq49rph4clwf6l2k5-gcc-4.6.4-HEAD

So you’ll get something like this:

$ tree /gnu/store/l612a4h9a6l4hs7kq49rph4clwf6l2k5-gcc-4.6.4-HEAD
/gnu/store/l612a4h9a6l4hs7kq49rph4clwf6l2k5-gcc-4.6.4-HEAD
├── bin
│   ├── riscv64-unknown-linux-gnu-cpp
│   ├── riscv64-unknown-linux-gnu-gcc
│   ├── riscv64-unknown-linux-gnu-gcc-4.6.4
│   └── riscv64-unknown-linux-gnu-gcov
├── etc
│   └── ld.so.cache
├── libexec
│   └── gcc
│       └── riscv64-unknown-linux-gnu
│           └── 4.6.4
│               ├── cc1
│               ├── collect2
│               ├── install-tools
│               │   ├── fixincl
│               │   ├── fixinc.sh
│               │   ├── mkheaders
│               │   └── mkinstalldirs
│               └── lto-wrapper
├── riscv64-unknown-linux-gnu
│   └── lib
└── share

...

16 directories, 28 files

If you want to try it, you can generate an extremely simple C file and give it a go:

$ cat <<END > hello.c
int main (int argc, char * argv[]){
    return 19;
}
END

$ /gnu/store/...-gcc-4.6.4-HEAD/bin/riscv64-unknown-linux-gnu-gcc -S hello.c
$ cat hello.s
.file   "hello.c"
    .option nopic
    .text
    .align  1
    .globl  main
    .type   main, @function
main:
    add sp,sp,-32
    sd  s0,24(sp)
    add s0,sp,32
    mv  a5,a0
    sd  a1,-32(s0)
    sw  a5,-20(s0)
    li  a5,19
    mv  a0,a5
    ld  s0,24(sp)
    add sp,sp,32
    jr  ra
    .size   main, .-main
    .ident  "GCC: (GNU) 4.6.4"

This can be later assembled and linked using binutils with not much trouble, as we might have introduced in the past.

Conclusion

The process as you can see is pretty much a pattern matching exercise, as I already mentioned in the beginning. Of course there were some places where I needed to review the different APIs and their implementation, but those were just a few. Not bad. We made this “work” in a short period of time and it looks pretty well.

Now I need to test this further, make more complex programs and try it, but it’s actually very difficult to do with the current compilation process because the standard C library is not found correctly and the assembler and the linker have to be dealt with independently. This means I need to fix the context first and then review the compiler itself.

On the other hand, the memory model related code, the builtins and the code I basically made up are worrying part of the project, because they might be a point of failure in the future. If they work only for optimizations and multithreading, that might not be an issue, because I don’t know how much of that is used in the GCC version we are going to compile with this compiler. Remember our backport’s only goal is to compiler a more recent GCC with it, so we don’t really need to care about other programs.

I already asked some people13 about the memory model parts and I got a very simple solution from them (basically forget about the memory models and always make a fence before and after synchronization code), so that’s going to be solved for the next post, and I can always review the builtins later if I need them.

The rest of the code looks like it would work in more complex cases, but still this needs proper testing and I need to be able to include the standard C library for that.

Reviewing the code

Of course, we are going to find bugs, and I did find some bugs in the development of the process. The code review is really hard to do so it’s better to use tricks and magic.

First of all, we need some debug symbols for gdb to find where the errors are and be able to debug them properly. The defined Guix package has a strip-binaries step that moves all the debug symbols to a separate folder:

$ guix build -f guix.scm
...
/gnu/store/gsq72r3xnv7b2f1l4z5idpy3j900hizk-gcc-4.6.4-HEAD-debug
/gnu/store/qglp0cx0nq2nblcg9ya4gmc5gfk2amjg-gcc-4.6.4-HEAD-lib
/gnu/store/l612a4h9a6l4hs7kq49rph4clwf6l2k5-gcc-4.6.4-HEAD

The debug directory there contains the debug symbols of the binaries so we can just call gdb and then use the symbol-file command to load the debug symbols associated with the program itself.

It is important to note that loading the gcc binary is a problem because it is a driver that execs other binaries, so the errors can’t be really followed properly. It’s better to choose the specific program we want to debug, normally cc1.

This happened to be extremely important because I forgot to convert one function to the old API and it was giving a segmentation fault. Using the GNU Debugger I found the source of the error and I just replaced formal arguments with the proper ones.

Last words

So, all that being said, we covered the changes, the possible problems, how to debug and what’s coming next. That was basically it.

If you have any question, suggestion, comment, or anything you want to share about this, contact me14. I’d be very happy to discuss.

From here, the plan is to review what I already did, test more complex software and share the results with you and also try to make the compilation process more reasonable. I hope it’s easier to do than it looks.

Wish me luck.


  1. 06166d9e5ff121fd3dfd6c0995621e557a023ef0 

  2. I screwed the ChangeLog files anyway LOL

  3. af295d607786f96b4e8f2e35f41ca34820a9aacb 

  4. 14577a05e3d64c9e2a05e8f0ff1f8965ddb27b68 

  5. 2b97a03a443fe8e408d7129bce9658032d0d9cd2 

  6. And I’m trying not to feel guilty for it. 

  7. There’s a great set of videos about GCC at the GCC Resource Center. They specifically talk about GCC 4.6! I watched them before going for the code and they helped me a lot to understand how was the code organized and how did GCC work. I recommend them a lot. 

  8. This local-file thing I learned from Efraim Flashner, currently a Guix maintainer, who gave a talk called “Compile it with Guix” where he introduces this method. Sadly, I can’t find the talk in the web to link you to it. 

  9. This process is that you compile GCC with the compiler you had (stage-1), then the resulting GCC compiles itself (stage-2), and the resulting GCC compiles itself again (stage-3). One way to make sure everything is correct is to compare the binary of the stage-2 and the stage-3. If they are the same, there are chances that our code is correct. If they are different, our code is wrong. GCC’s compilation framework does this automatically (if --disable-bootstrap is not set) but, you can’t do it when cross-compiling, because there’s no way to run the stage-1 compiler. I would like to see the result of this process, but I can’t at the moment. 

  10. See? That’s why I try to write blog posts about the things I do, that way I don’t forget things. It was too late for this. 

  11. These .def files are a lot of fun in GCC’s codebase. They appear really often. They are files that look like a bunch of similar function calls but what they actually are macro calls. Then, this files are #included into another file right after the macro is defined so they generate code. Later, you can redefine the macro to create some other output and #include them again so they’ll always generate coherent code. This is used a lot on enums and switch-case statements, if you want them both to be coherent, you can move them to a .def file, define all the possible values of the enum there, and generate first the enum with the first #include and later the switch-case with a new #include later. Take a look to gcc/rtl.c and you’ll see what I mean. (Yes I know this is like hardcore magic and it’s hard to understand, I didn’t choose to do this). 

  12. I say “hard to match” because searching for TARGET_MUL or MASK_MUL gave NO results, and searching for MUL gave too many. 

  13. I asked Andrew Waterman himself (one of the authors of RISC-V, and the current maintainer of the RISC-V GCC target). Yep, and he actually answered. 

  14. You can find my contact info in the About page